package com.atguigu.gmall.gateway.filter;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.atguigu.gmall.common.result.Result;
import com.atguigu.gmall.common.result.ResultCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Collection;
import java.util.List;

/**
 * 全局过滤器:验证经过网关请求用户身份
 *
 * @author: atguigu
 * @create: 2023-06-20 14:42
 */
@Slf4j
@Component
public class AuthFilter implements GlobalFilter, Ordered {


    @Autowired
    private RedisTemplate redisTemplate;


    private static AntPathMatcher antPathMatcher = new AntPathMatcher();

    /**
     * 存放需要登录才能访问页面
     */
    @Value("${authUrls.url}")
    private List<String> authUrls;


    /**
     * 过滤器业务逻辑
     *
     * @param exchange 封装请求,响应对象
     * @param chain    网关过滤器链
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        //1.获取用户访问路径(客户端提交同步页面请求,异步Ajax请求等)
        String path = request.getURI().getPath();
        log.info("用户请求访问路径:{}", path);
        //2.前端页面中css,js,img直接放行-不需要身份验证
        if (antPathMatcher.match("/css/**", path) || antPathMatcher.match("/js/**", path) || antPathMatcher.match("/img/**", path)) {
            //执行放行
            return chain.filter(exchange);
        }
        //3.请求地址中包含"inner"请求(两个微服务间业务调用)不允许客户端进行调用-无论登录与否
        if (antPathMatcher.match("/**/inner/**", path)) {
            //给出错误提示信息,没有权限访问
            return outError(response, ResultCodeEnum.ILLEGAL_REQUEST);
        }

        //4.从网关中查询Redis根据客户端提交Token令牌 判断是否登录
        String userId = this.getUserId(request);

        if (StringUtils.isBlank(userId)) {
            //5.如果用户未登录,访问一些需要登录后才能访问同步页面请求,重定向到登录页面
            if (!CollectionUtils.isEmpty(authUrls)) {
                for (String authUrl : authUrls) {
                    if (antPathMatcher.match("/" + authUrl, path)) {
                        //重定向跳转到登录页面,让用户登录
                        response.setStatusCode(HttpStatus.SEE_OTHER); //Http状态码 401
                        //通过响应头设置需要跳转页面
                        response.getHeaders().add(HttpHeaders.LOCATION, "http://passport.gmall.com/login.html?originUrl=" + request.getURI());
                        return response.setComplete();
                    }
                }
            }
            //6.如果用户未登录,访问一些需要登录后才能访问同步后端Restful接口("/auth"),提示没有权限访问提示信息
            if (antPathMatcher.match("/**/auth/**", path)) {
                //给出错误提示信息,没有权限访问
                return outError(response, ResultCodeEnum.LOGIN_AUTH);
            }
        }

        //7.除此以外请求,放行将网关中获取到用户ID"透传"到下游目标业务微服务
        request.mutate().header("userId", userId);

        //8.尝试获取临时用户ID
        String userTempId = getUserTempId(request);
        request.mutate().header("userTempId", userTempId);

        return chain.filter(exchange);
    }

    /**
     * 尝试根据请求对象中请求头token或者Cookie中token查询Redis获取用户信息
     *
     * @param request
     * @return
     */
    private String getUserId(ServerHttpRequest request) {
        //1.先尝试从请求头中获取token
        String token = request.getHeaders().getFirst("token");
        //2.请求头中如果没有token再从Cookie中尝试获取
        if (StringUtils.isBlank(token)) {
            HttpCookie cookie = request.getCookies().getFirst("token");
            if (cookie != null) {
                token = cookie.getValue();
            }
        }
        //3.查询Redis中存放用户信息,将用户ID获取
        String key = "user:" + token;
        JSONObject jsonObject = (JSONObject) redisTemplate.opsForValue().get(key);
        if (jsonObject != null) {
            return jsonObject.getString("id");
        }
        return null;
    }


    /**
     * 静态页HTML同步请求,临时用户会在Cookie中提交
     * ajax请求,直接在请求头中提交
     * 尝试根据请求对象中请求头userTempId或者Cookie中userTempId获取临时用户ID
     *
     * @param request
     * @return
     */
    private String getUserTempId(ServerHttpRequest request) {
        //1.先尝试从请求头中获取token
        String userTempId = request.getHeaders().getFirst("userTempId");
        //2.请求头中如果没有token再从Cookie中尝试获取
        if (StringUtils.isBlank(userTempId)) {
            HttpCookie cookie = request.getCookies().getFirst("userTempId");
            if (cookie != null) {
                userTempId = cookie.getValue();
            }
        }
        return userTempId;
    }

    /**
     * 用来给前端响应错误提示信息
     *
     * @param response
     * @param resultCodeEnum
     * @return
     */
    private Mono<Void> outError(ServerHttpResponse response, ResultCodeEnum resultCodeEnum) {
        //1.准备响应结果对象,转为JSON对象
        Result<Object> result = Result.build(null, resultCodeEnum);
        String resultString = JSON.toJSONString(result);

        //2.响应结果给客户端
        //2.1 设置http状态码
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        //2.2 通过响应头设置响应数据格式-json
        response.getHeaders().add("content-type", "application/json;charset=utf-8");
        DataBuffer wrap = response.bufferFactory().wrap(resultString.getBytes());
        //2.3 网关结束响应-不再路由转发
        //return response.setComplete();
        //2.4 网关将响应数据返回给客户端
        return response.writeWith(Mono.just(wrap));
    }

    /***
     * 过滤器执行属性 值越小 优先级越高
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }

    public static void main(String[] args) {
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        boolean match = antPathMatcher.match("/**/inner/**", "/api/product/inner/getSkuInfo/24");
        System.out.println(match);
    }
}
